package edu.northwestern.cbits.purple_robot_manager.probes.devices; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TimeZone; import java.util.UUID; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.hardware.SensorEvent; import android.net.Uri; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.Preference; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import com.getpebble.android.kit.PebbleKit; import com.getpebble.android.kit.PebbleKit.FirmwareVersionInfo; import com.getpebble.android.kit.PebbleKit.PebbleDataLogReceiver; import com.getpebble.android.kit.util.PebbleDictionary; import edu.northwestern.cbits.purple_robot_manager.R; import edu.northwestern.cbits.purple_robot_manager.activities.RealTimeProbeViewActivity; import edu.northwestern.cbits.purple_robot_manager.calibration.PebbleCalibrationHelper; import edu.northwestern.cbits.purple_robot_manager.db.ProbeValuesProvider; import edu.northwestern.cbits.purple_robot_manager.logging.LogManager; import edu.northwestern.cbits.purple_robot_manager.probes.Probe; import edu.northwestern.cbits.purple_robot_manager.probes.builtin.Continuous3DProbe; import edu.northwestern.cbits.purple_robot_manager.probes.builtin.ContinuousProbe; public class PebbleProbe extends Continuous3DProbe { public final static String PROBE_NAME = "edu.northwestern.cbits.purple_robot_manager.probes.devices.PebbleProbe"; private static final String FIRMWARE_VERSION = "FIRMWARE_VERSION"; private static final String BUNDLE_IS_CHARGING = "IS_CHARGING"; private static final String BUNDLE_CHARGE_LEVEL = "CHARGE_LEVEL"; private static final byte COMMAND_FETCH_BATTERY = 0x00; private static UUID WATCHAPP_UUID = UUID.fromString("3cab0453-ff04-4594-8223-fa357112c305"); public static final String ENABLED = "config_probe_pebble_enabled"; public static final boolean DEFAULT_ENABLED = false; private static final int BUFFER_SIZE = 20; private static final String DB_TABLE = "pebble_probe"; private final double valueBuffer[][] = new double[3][BUFFER_SIZE]; private final double timeBuffer[] = new double[BUFFER_SIZE]; private PebbleDataLogReceiver _logReceiver = null; private int _index = 0; private Map<String, String> _schema = null; private PebbleKit.PebbleDataReceiver _messageReceiver = null; private PebbleKit.PebbleNackReceiver _nackReceiver = null; private PebbleKit.PebbleAckReceiver _ackReceiver = null; private boolean _isCharging = false; private int _chargeLevel = -1; private long _lastRefresh = 0; @Override public boolean getUsesThread() { return false; } private static class AccelData { // TODO: Credit https://github.com/kramimus/pebble-accel-analyzer final private int x; final private int y; final private int z; private long timestamp = 0; final private boolean didVibrate; public AccelData(byte[] data) { x = (data[0] & 0xff) | (data[1] << 8); y = (data[2] & 0xff) | (data[3] << 8); z = (data[4] & 0xff) | (data[5] << 8); didVibrate = data[6] != 0; for (int i = 0; i < 8; i++) { timestamp |= ((long) (data[i + 7] & 0xff)) << (i * 8); } } @SuppressWarnings("unused") public JSONObject toJson(Context context) { JSONObject json = new JSONObject(); try { json.put("x", x); json.put("y", y); json.put("z", z); json.put("ts", timestamp); json.put("v", didVibrate); return json; } catch (JSONException e) { LogManager.getInstance(context).logException(e); } return null; } public static List<AccelData> fromDataArray(byte[] data) { List<AccelData> accels = new ArrayList<>(); for (int i = 0; i < data.length; i += 15) { accels.add(new AccelData(Arrays.copyOfRange(data, i, i + 15))); } return accels; } public long getTimestamp() { return timestamp; } public void applyTimezone(TimeZone tz) { timestamp -= tz.getOffset(timestamp); } } @Override public String name(Context context) { return "edu.northwestern.cbits.purple_robot_manager.probes.devices.PebbleProbe"; } @Override public String probeCategory(Context context) { return context.getResources().getString(R.string.probe_other_devices_category); } @Override @SuppressWarnings("deprecation") public PreferenceScreen preferenceScreen(final Context context, PreferenceManager manager) { PreferenceScreen screen = super.preferenceScreen(context, manager, false); screen.setTitle(this.title(context)); screen.setSummary(R.string.summary_pebble_probe_desc); CheckBoxPreference enabled = new CheckBoxPreference(context); enabled.setTitle(R.string.title_enable_probe); enabled.setKey(PebbleProbe.ENABLED); enabled.setDefaultValue(PebbleProbe.DEFAULT_ENABLED); screen.addPreference(enabled); Preference installWatchApp = new Preference(context); installWatchApp.setTitle(R.string.probe_pebble_install_label); installWatchApp.setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(context.getString(R.string.probe_pebble_install_url)))); screen.addPreference(installWatchApp); return screen; } @Override public void enable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(PebbleProbe.ENABLED, true); e.commit(); } @Override public void disable(Context context) { SharedPreferences prefs = Probe.getPreferences(context); Editor e = prefs.edit(); e.putBoolean(PebbleProbe.ENABLED, false); e.commit(); } @Override public boolean isEnabled(Context context) { SharedPreferences prefs = Probe.getPreferences(context); if (super.isEnabled(context)) { if (prefs.getBoolean(PebbleProbe.ENABLED, PebbleProbe.DEFAULT_ENABLED)) { PebbleCalibrationHelper.check(context, true); PebbleKit.startAppOnPebble(context, PebbleProbe.WATCHAPP_UUID); if (this._logReceiver == null) { final PebbleProbe me = this; this._logReceiver = new PebbleDataLogReceiver(WATCHAPP_UUID) { @Override public void receiveData(final Context context, UUID logUuid, final Long timestamp, final Long tag, final byte[] payload) { synchronized (me) { TimeZone here = Calendar.getInstance().getTimeZone(); List<AccelData> accels = AccelData.fromDataArray(payload); for (AccelData accel : accels) { if (me._index >= PebbleProbe.BUFFER_SIZE) { long now = System.currentTimeMillis(); Bundle data = new Bundle(); data.putDouble(Probe.BUNDLE_TIMESTAMP, now / 1000); data.putString(Probe.BUNDLE_PROBE, me.name(me._context)); data.putBoolean(PebbleProbe.BUNDLE_IS_CHARGING, me._isCharging); data.putInt(PebbleProbe.BUNDLE_CHARGE_LEVEL, me._chargeLevel); FirmwareVersionInfo info = PebbleKit.getWatchFWVersion(context); if (info != null) data.putString(PebbleProbe.FIRMWARE_VERSION, "" + info.getMajor() + "." + info.getMinor() + "." + info.getPoint()); data.putDoubleArray(ContinuousProbe.SENSOR_TIMESTAMP, timeBuffer); for (int i = 0; i < fieldNames.length; i++) { data.putDoubleArray(fieldNames[i], valueBuffer[i]); } me.transmitData(context, data); me._index = 0; } accel.applyTimezone(here); timeBuffer[me._index] = accel.getTimestamp(); double x = 9.807 * ((double) accel.x) / 1000; double y = 9.807 * ((double) accel.y) / 1000; double z = 9.807 * ((double) accel.z) / 1000; valueBuffer[0][me._index] = x; valueBuffer[1][me._index] = y; valueBuffer[2][me._index] = z; if (me._index % 10 == 0) { Map<String, Object> values = new HashMap<>(4); values.put(Continuous3DProbe.X_KEY, x); values.put(Continuous3DProbe.Y_KEY, y); values.put(Continuous3DProbe.Z_KEY, z); values.put(ProbeValuesProvider.TIMESTAMP, (double) (accel.getTimestamp() / 1000)); ProbeValuesProvider.getProvider(context).insertValue(context, PebbleProbe.DB_TABLE, me.databaseSchema(), values); double[] plotValues = { timeBuffer[0] / 1000, x, y, z }; RealTimeProbeViewActivity.plotIfVisible(me.getTitleResource(), plotValues); } me._index += 1; } } } }; PebbleKit.registerDataLogReceiver(context, this._logReceiver); } if (this._ackReceiver == null) { this._ackReceiver = new PebbleKit.PebbleAckReceiver(PebbleProbe.WATCHAPP_UUID) { public void receiveAck(Context context, int i) { } }; PebbleKit.registerReceivedAckHandler(context, this._ackReceiver); } if (this._nackReceiver == null) { this._nackReceiver = new PebbleKit.PebbleNackReceiver(PebbleProbe.WATCHAPP_UUID) { public void receiveNack(Context context, int i) { } }; PebbleKit.registerReceivedNackHandler(context, this._nackReceiver); } if (this._messageReceiver == null) { final PebbleProbe me = this; this._messageReceiver = new PebbleKit.PebbleDataReceiver(PebbleProbe.WATCHAPP_UUID) { public void receiveData(final Context context, final int transactionId, final PebbleDictionary dictionary) { PebbleKit.sendAckToPebble(context, transactionId); byte[] payload = dictionary.getBytes(1); me._isCharging = ((payload[0] & 0x80) == 0x80); me._chargeLevel = payload[0] & 0x7f; } }; PebbleKit.registerReceivedDataHandler(context, this._messageReceiver); } long now = System.currentTimeMillis(); if (now - this._lastRefresh > 5 * 60 * 1000) { PebbleDictionary data = new PebbleDictionary(); data.addUint8(0, PebbleProbe.COMMAND_FETCH_BATTERY); PebbleKit.sendDataToPebble(context, PebbleProbe.WATCHAPP_UUID, data); } return true; } if (this._logReceiver != null) { try { context.unregisterReceiver(this._logReceiver); } catch (IllegalArgumentException e) { // Do nothing - receiver not registered... } this._logReceiver = null; } } PebbleCalibrationHelper.check(context, false); return false; } @Override protected String tableName() { return PebbleProbe.DB_TABLE; } @Override protected Map<String, String> databaseSchema() { if (this._schema == null) { this._schema = new HashMap<>(); this._schema.put(Continuous3DProbe.X_KEY, ProbeValuesProvider.REAL_TYPE); this._schema.put(Continuous3DProbe.Y_KEY, ProbeValuesProvider.REAL_TYPE); this._schema.put(Continuous3DProbe.Z_KEY, ProbeValuesProvider.REAL_TYPE); } return this._schema; } @Override public long getFrequency() { return 0; } @Override public int getTitleResource() { return R.string.title_pebble_probe; } @Override public int getSummaryResource() { return R.string.summary_pebble_probe_desc; } @Override public String getPreferenceKey() { return "pebble"; } @Override protected boolean passesThreshold(SensorEvent event) { return true; } @Override public String summarizeValue(Context context, Bundle bundle) { double xReading = bundle.getDoubleArray("X")[0]; double yReading = bundle.getDoubleArray("Y")[0]; double zReading = bundle.getDoubleArray("Z")[0]; return String.format(context.getResources().getString(R.string.summary_accelerator_probe), xReading, yReading, zReading); } @Override protected double getThreshold() { return 0; } @Override protected int getResourceThresholdValues() { return -1; } @Override public int getResourceFrequencyValues() { return -1; } }